1 - Introduction

react本身是一个UI库,它没有专门的数据获取模式,虽然说获取数据之后,我们可以使用redux这些状态管理库来处理数据在整个应用的共享,但是这些状态管理库本身对client state管理的好(比如说theme),但是对于数据获取,异步、服务器状态不是很管用。

image-20251107204318977

React 工程中一个非常核心的设计点:数据获取(data fetching)和状态管理(state management) 我们来系统地解释一下👇


🧩 一、React 自身的问题:数据获取的痛点

React 本身是一个UI 库,它不负责数据获取或缓存逻辑。当项目开始复杂化(尤其是涉及接口调用)时,就会暴露出几个典型问题。

假设你直接用 useEffect + fetch + useState 来请求接口:

看似简单,但一旦项目复杂起来,就会出现以下问题👇:

问题说明
🔁 重复请求同一个接口可能在多个组件中都被请求多次
🧠 缓存难管理要自己管理缓存、过期、刷新等逻辑
🕐 重新请求逻辑繁琐比如窗口重新聚焦、数据变更后重新加载都要手写
全局状态难统一请求状态(loading、error、data)常常散落在各处
♻️ 刷新与同步困难用户操作后(如新增、删除)如何让列表自动刷新?

这些都是 React 本身不擅长解决的问题。 于是,就有了 React Query(现在叫 TanStack Query)。


🚀 二、React Query 是什么?

React Query 是一个用于 数据获取(server state management) 的库。 它的核心目标是:让你不再手写 useEffect 来获取数据。

一句话总结:

🧠 React Query 让你只关心“数据是什么”,而不用关心“什么时候、怎么获取、怎么缓存”。


🎯 三、React Query 解决了哪些问题?

1️⃣ 自动管理请求的状态

你不用再写一堆 loadingerroruseEffect

React Query 自动帮你:


2️⃣ 缓存与共享

同一个 queryKey 的数据会自动缓存,跨组件共享。

不论在哪个组件中调用,只要 key 一样,就共享同一份数据缓存。


3️⃣ 自动刷新与失效

例如当你新增了一个用户:

➡️ 自动让 UserList 重新获取最新数据!


4️⃣ 并行与依赖请求

你可以轻松写出:


5️⃣ SSR、分页、预取

React Query 还支持:

这些都大大提升了性能与开发体验。


🧱 四、React Query vs 全局状态库(Redux、Zustand)

类型适合管理的数据说明
React Query服务端状态(Server State)从 API 获取的、可能被服务器更新的数据
Redux / Zustand / Jotai客户端状态(Client State)用户登录态、UI 开关、临时表单状态等

📌 一句话记住:

✅ “接口数据”用 React Query, ✅ “本地交互状态”用 Redux/Zustand。


🌍 五、React Query 的核心理念

让你的 React 应用 以声明式的方式 处理“数据获取”这件事。

你只需要告诉它:


✅ 总结

问题React Query 的解决方式
手动写 useEffect提供 useQuery 自动处理
重复请求内置缓存与共享
缺乏状态管理自动提供 isLoading/error/data
刷新困难invalidateQueries / 自动刷新机制
性能低缓存、请求去重、后台刷新

这里出现了client state和server state,之前没有了解过这些概念,这个概念在react中还是蛮重要的。

image-20251107204437803

在 React 应用中,确实存在两种性质完全不同的状态:

👉 Client State(客户端状态) 👉 Server State(服务端状态)

它们看起来都叫 “state”,但本质、生命周期、管理方式完全不同。 我们来一步步拆开讲 👇


🧠 一、什么是 Client State(客户端状态)

Client State 指的是在浏览器或 React 应用内部自己管理的数据 它完全存在于前端,不依赖后端。

✅ 典型例子:

🧩 特点:

特征说明
💾 存在位置浏览器内存(React 组件、Context、Redux、Zustand 等)
🔄 更新方式前端代码直接修改
⏳ 生命周期跟随页面刷新而清空
🚫 不需要 API不依赖后端
🧰 管理工具useState / useReducer / Context / Redux / Zustand / Jotai 等

🧠 举例:

这些都是典型的 Client State。 前端知道怎么改,后端根本不关心。


🌍 二、什么是 Server State(服务端状态)

Server State 是从服务器获取的数据,它存在于后端系统,前端只是读取或缓存它。

✅ 典型例子:

🧩 特点:

特征说明
📡 来源由服务器提供,通过 API 获取
🧠 管理难度难(需要处理 loading、error、缓存、同步等)
♻️ 更新方式必须请求或刷新 API
🔁 数据变化后端更新后,前端需要重新同步
⚙️ 管理工具React Query / SWR / Apollo / Relay 等

🧠 举例:

这些数据不是前端控制的,而是服务器控制的。 前端只能请求 → 缓存 → 刷新。


⚖️ 三、两者的区别总结表

对比项Client StateServer State
数据来源浏览器内存、本地逻辑服务器、数据库
更新方式前端逻辑直接 setState通过 API 调用
是否可持久化否(刷新即丢)是(保存在后端)
是否可共享通常只在当前页面内多用户、多客户端共享
典型工具useState / Redux / ZustandReact Query / SWR / Apollo
主要难点结构设计、性能优化缓存、同步、失效、刷新

🚀 四、为什么要区分这两种状态?

在 React 早期(比如 Redux 盛行时代),很多人把两者混在一起用:

后来社区逐渐达成共识:

🧭 Client State → 由前端状态库管理 🧭 Server State → 交给 React Query / SWR 等库管理

这样职责清晰、逻辑更简单。


🔧 五、一个直观的对比例子

假设你有一个「用户列表」页面,可以点击展开用户详情弹窗:

🔹 users → Server State(后端控制) 🔹 selectedUser → Client State(前端控制)


💡 六、结论总结

概念定义管理方式常用库
Client StateUI 内部、用户交互、表单状态useState / Context / Redux / ZustandReact 内置
Server State由后端维护,通过 API 获取的数据React Query / SWR / ApolloTanStack Query

React 负责渲染 UI Client State 控制交互逻辑 Server State 控制数据同步

 

课程内容如下,还是蛮丰富的:

image-20251107204618705

2 - Project Setup

创建项目,如果不想创建项目,老师的github里面提供了react-query-starter的文件夹,可以直接使用。

image-20251107204848710

1、创建:npm create vite@lastest react-query-demo

2、使用json-server来创建接口,在项目根目录创建db.json文件,从老师的文件把数据拷贝过来。然后创建脚本命令,以后运行npm run json-server就可以启动服务端了。

image-20251107205353192

3、安装react路由npm i react-router,在main.tsx里面添加路由。

image-20251107210043332

创建三个简单的组件/src/components/Home.tsxsrc\components\RQSuperHeroes.tsxsrc\components\SuperHeroes.tsx,里面写最简单的代码即可。

然后在App.tsx里面编写路由。

然后按照老师的样式修改App.css和index.css就行了。运行项目:

 

来看一下使用useStateuseEffect来获取数据是什么样的:

安装axiosnpm i axios

 

3 - Fetching Data with useQuery

react-query在2022年停止更新了,文档推荐使用@tanstack/react-query

安装依赖:npm i @tanstack/react-query

没想到技术更新这么快,我看一下能不能按照老师的课程标题来使用这个新的库学习,先做着吧。

1、引入Provider,创建client,为应用提供client

2、使用useQuery来获取数据

可以将queryFn抽离出来:

4 - Handling Query Error

传统处理错误的方式

定义error状态,然后在请求数据的时候catch,如果有error就展示。

image-20251108085133949

效果:

useQuery中处理错误的方式

useQuery的返回值有errorisError,可以使用它们来处理错误。

image-20251108090033808

 

5 - React Query Devtools

安装npm i @tanstack/react-query-devtools

在App.tsx中引入并使用。

可以看到,在页面的右下角,出现了一个button,打开可以看到发起的请求信息。后面会详细介绍这个工具的使用方法。

6 - Query Cache

使用ctrl+shift+r,清除缓存重新加载,然后查看传统请求和useQuery的区别。

我在两个接口请求时,都添加了延时,可以看到传统方式一直显示loading,而useQuery在第一次显示loading之后,就不再显示了,这就是cache缓存的作用。

原理是这样的:当某个queryKey的请求触发之后,isLoading被设置为true,网络请求被发起。当网络请求完成后,react-query会根据queryKey和queryFn将请求结果缓存下来,当重新触发这个请求时,react-query会检查是否存在符合queryKey和queryFn缓存的数据,如果存在,缓存数据会被返回,此时的isLoading不会变为true。react-query知道数据可能会有变化,缓存的数据可能不是最新的,所以在background会触发这个请求,当请求成功后,会在UI上渲染最新的数据。

因为我们的数据没有变化,所以没有看到UI的变化。那有没有东西可以证明background的重新请求发生了呢?有,useQuery返回一个isFetching,可以帮助我们理解。

可以看到,当第一次请求时,isLoading是正常工作的,先是true,请求成功之后变为false。isFetching也是一样。

接下来我们把db.json里面的数据改一下,重新访问这个页面,看一下效果:

可以看到,使用了缓存的数据,数据请求成功之后会更新UI。isLoading一直是false。isFetching先是true,后是false。

react-query缓存的时间默认是300000ms,也就是5分钟。如果想要修改缓存时间,可以向useQuery传入参数gcTime,时间单位是ms。

比如说我们设置gcTime: 5000,缓存时间改为5s,看一下效果:

可以看到,5s后这个请求缓存会被垃圾回收处理。

这种缓存做的很好,只在第一次触发loading的显示,后面都是在后台默默的刷新,只要接口返回数据快,我感觉显示效果很好。

7 - Stale Time

stale time:陈旧时间。在数据缓存Web 开发中非常重要的概念,,它用来定义数据在被认为是“新鲜”的时长。决定了你的应用在多长时间内会信任缓存中的数据,而不会尝试去重新获取新数据。

如果用户可以在一段时间内看缓存的数据,那么我们就没有必要在后台重新请求数据。这时候可以为useQuery设置staleTime参数,设置多长时间内不重新请求数据,单位是ms,默认值为0。

可以看到,第一次请求数据时,network里面有显示,但第二次就没有了,说明在stale time内,不会发起请求。

8 - Refetch Defaults

这节课再来看一些refetch相关的默认设置。

refetchOnMount

作用是控制当一个组件“挂载”(Mount)时,是否应该重新触发一次数据请求。默认是true。

组件“挂载”通常发生在:

  1. 组件第一次被渲染到屏幕上时。
  2. 用户离开一个页面,然后又返回到这个页面时(导致组件被卸载后又重新挂载)。

它的值有true | false | "always",当设置为always时,当组件挂载时,总是重新获取数据。它会无视 staleTime。不管数据是不是“新鲜”的 (fresh),只要组件一挂载,就立刻强制发起一次新的网络请求。

refetchOnWindowFocus

作用:当浏览器窗口或标签页重新获得焦点(Focus)时,检查并重新获取已挂载查询的数据。

值有true | false | "always"。当设置为always时,强制重新获取窗口聚焦时,无视 staleTime,总是触发重新获取。

场景举例

想象一个用户正在使用你的应用,然后在后台打开了一个新的标签页,或者切换到了另一个程序,几分钟后用户又:

  1. 点击 了你的浏览器标签页。
  2. Alt + Tab 切换回了你的应用程序窗口。
  3. 点击 了页面上的空白区域使页面重新聚焦。

一旦发生上述操作,如果设置了 refetchOnWindowFocusReact Query 就会在后台自动触发一次数据请求,将当前页面上的旧数据更新为最新数据。

看一下传统数据请求在用户切换窗口之后,会不会重新请求数据。我先打开Traditional Super Heroes页面,然后在db.json里面修改数据,看会不会重新获取数据。

可以看到,没有。

react query里面的表现,可以看到重新请求了数据。

9 - Polling

Polling (轮询) 是一种非常基础且常见的网络通信技术,用于在客户端(如浏览器、App)和服务器之间同步数据或检查状态。轮询就是客户端每隔固定的时间间隔,主动向服务器发送请求,询问“是否有新数据或新状态?”。

比如说我展示一个股票的界面,需要隔一段时间显示最新的数据,这时候的需求就是不断请求数据。

那在react query里面怎么做到呢?可以设置refetchInterval参数,默认是false,如果设置为数字,单位为ms。

可以看到,每隔2s,会重新请求数据。

当浏览器没有focus的时候,即使设置了refetchInterval,请求也会中断。如果想在浏览器没有focus的时候,依然请求数据,这时候可以设置refetchIntervalInBackground: true

题外话

除了轮询,还有什么技术?

技术方式实时性典型场景
Polling (轮询)客户端主动问:“好了吗?” (每 N 秒问一次)简单的状态检查,或低实时性要求。
Long Polling (长轮询)客户端问:“好了吗?” 服务器先不回,直到有数据或超时才回。中高在 WebSocket 不支持时的替代方案。
WebSockets双向持久连接,服务器随时可以推送数据。极高 (实时)实时聊天、多人游戏、股票行情。
Server-Sent Events (SSE)单向持久连接,服务器可以主动向客户端推送数据。新闻推送、实时更新的仪表盘。

10 - useQuery on click

之前触发网络请求,要么是组件onMount时、要么是window focus时。如果我想在点击某个元素的时候触发网络请求,怎么做呢?

1、设置useQuery不要再组件onMount时请求数据,使用enabled参数

2、使用useQuery的返回值refetch,可以手动触发网络请求

可以看到,手动触发之后才发起请求。并且staleTime和cache也是同样工作的。

如果想在再次手动触发的时候显示loading效果,可以使用isFetching来帮忙。

11 - Success and Error Callbacks

在数据请求之后,我们可能想做一些副作用,比如说打开一个modal、跳转到一个新路由、或者显示toast提示信息。

原来的做法:

image-20251108114306928

但是在@tanstack/react-queryv5里面的useQuery里面的onSuccessonError已经移除了,但在useMutation里面还保留着。

因为这些回调不会在数据从缓存中直接读取时触发。比如数据被缓存后,再次同 key 请求可能直接走缓存,不会触发 onSuccess

那这里该怎么写呢?我觉得onSuccess可以直接使用isSuccess来代替,而onError可以用isErrorerror来代替,结合useEffect来使用。

先看请求成功,然后修改请求地址看请求失败的情况:

可以看到,请求失败之后,会重试3次,3次再失败才会变更isError的值。

12 - Data Transformation

这节课学习转变接口返回数据的格式。

可以使用useQueryselect参数,传递一个函数进去,这个函数可以接收到接口返回的数据。这时候useQuery的ts类型,需要传递第3个泛型进去。

假设需求是:将superheroes的返回结果改为字符串数组,这样就不需要使用hero.name来访问了。

image-20251108132410037

13 - Custom Query Hook

这节课学习怎么包裹useQuery这个hook,创建自定义的hook,这样在不同的组件里面,我们就可以使用自定义hook来调用接口了。

1、创建hook

因为hook就是一个函数,我们要的最终返回结果是什么呢?其实就是useQuery的调用过程,这一点是最重要的,要搞清楚。

注意:自定义hook中可以设置参数,然后用到useQuery里面就行了。这就是函数柯里化。

2、然后在不同的组件里面使用自定义hook即可

工作正常:

image-20251108134426450

14 - Query by Id

这节课学习怎么使用id来查询详情数据。需要做下面的事情:

image-20251108134650838

1、创建RQSuperHeroDetail.tsx文件

2、在App.tsx里面添加路由,在RQSuperHeroes页面添加跳转链接

image-20251108135507432

可以看到,跳转正常:

3、编写自定义hook,获取详情数据

4、在页面中使用自定义hook获取数据

可以看到,获取了详情数据。

注意:在指定queryFn的函数时,可以不传递参数,useQuery会将参数传递过来,里面有一个querykey的参数,是一个数据,获取它的第二个参数即可。

image-20251108144615336

image-20251108144822121

15 - Parallel Queries

有时候,一个组件里面需要调用多个API接口来获取需要的数据,多个接口之间没有相互关系,所以称它们为平行请求。

创建新组件:

App.tsx里面添加新路由<Route path="/rq-parallel" element={<ParallelQueries />} />

需求就是在新组件里面,请求superheroes和friends的数据。

16 - Dynamic Parallel Queries

动态平行查询。

案例:我DynamicParallel页面,请求多个superhero的详情数据,传递的heroIds的长度是未知的。所以不能像上节课那样,使用多个useQuery来解决问题,而是需要使用useQueries

添加路由:

编写组件:

可以看到,两个请求发起了。

如果多个请求返回的结果结构类似,还可以使用combine方法,将多个返回结果合并成一个:

image-20251108153853907

实际应用场景:

一个组件中需要同时加载多个接口的数据。比如:

那么就可以写成这样:

results 是一个数组:

17 - Dependent Queries

有时候,我们需要按照顺序请求数据,下一个请求的参数要依靠上一个请求的返回结果。

在db.json里面新增数据:

image-20251108154937621

需求:先根据email获取user信息,然后根据channelId获取courses信息。

1、添加路由:

2、获取数据

还是使用两个useQuery,核心是:在第二个请求中,设置enabled: !!channelId,这样当channelId没有值时,第二个请求是不会发起的。

这两个接口的执行流程到底是什么呢?

执行流程全过程(从组件 mount 开始,每一毫秒都说清楚,下面的时间ms都是模拟说明的文字)

时间点发生了什么query1 状态query2 状态网络请求控制台会打印什么(如果你加了 console.log)
0ms 组件刚 mountReact Query 创建两个 query 对象,但 只有 enabled=true 的才会启动status: 'loading' isFetching: truestatus: 'idle' isFetching: false只发 接口1 的请求query1: loading query2: idle(完全不存在)
100ms 接口1 还在请求中query1 正在 pending,query2 看到 query1.data 是 undefined → !!undefined = falseloadingidle(不会创建 observer)只有接口1在飞query2 enabled=false,不发请求
800ms 接口1 请求成功返回query1.data = { id: 42 } React 重新 render → !!query1.data?.id 从 false 变成 truestatus: 'success' data: {id:42}瞬间从 idle → loading React Query 自动创建 observer 并执行 queryFn立刻发接口2 的请求(零延迟)query1: success query2: 条件满足!从 idle → loading,发起 fetchUserDetail(42)
850ms 接口2 请求中query1 已成功,query2 正在 pendingsuccessloading isFetching: true接口2 在飞query2: 正在获取详情...
1500ms 接口2 成功返回query2.data = { name: 'Grok', level: 99 }successstatus: 'success' isFetching: false两个请求都完成query2: success,拿到数据啦!
之后 组件 rerender 或页面刷新由于 queryKey 已缓存,两个接口都不会再发请求(除非 staleTime 到期)success (from cache)success (from cache)零请求query1: 从缓存读取 query2: enabled=true,直接从缓存读

你只管写 enabled: !!dep,React Query 帮你处理了“创建/销毁/订阅/缓存/重试”所有脏活!

每个 useQuery 钩子 = React Query 在内部 new QueryObserver() 一个实例,它负责:

18 - Initial Query Data

这节课学习怎么为query请求提供初始的查询数据,当从superheroes列表访问详情的时候,我们可以使用列表里面的一些数据,提供给详情界面先显示,然后里面请求更具体的详情数据。

1、使用queryClientgetQueryData方法,拿到列表的数据

image-20251108164258871

2、使用拿到的数据,设置useQueryinitialData的值

可以看到,数据显示了,但是network里面过了一段时间才发起请求,说明初始值成功赋值。

19 - Paginated Queries

这节课学习分页请求。

1、添加路由:

2、编写组件

json server的分页这样写:http://localhost:4000/colors?_page=2&_per_page=2

思路是创建一个页码的状态,把这个状态传递给请求数据的函数。当这个状态变化时,组件会重新渲染,那么useQuery会接收到最新的页码来请求数据。流程并不复杂。

可以看到,分页效果很好。

但是有个问题,就是每次请求新数据的时候,都有Loading,能不能不显示loading,而是显示之前的数据,可以使用placeholderData属性。(我觉得显示loading才是正常的,显示之前的数据可能让用户觉得点击按钮不起作用,会一直点击)

image-20251108191051169

可以看到,向前翻页的时候,没有显示Loading。

20 - Infinite Queries

适用于“无限滚动”的场景。

需求:在colors下面添加一个load more的按钮,每次点击这个按钮,会新增显示两个colors。

1、添加路由

2、编写组件,使用useInfiniteQuery来实现“无限滚动”的场景

21 - Mutations

前面20节课,讲了获取数据,接下来讲解提交post数据。

在react query中,mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, TanStack Query exports a useMutation hook.

1、创建自定义提交hook

2、在RQSuperHeroes页面创建提交UI,调用自定义提交HOOK

可以看到,新增成功之后,手动refetch,就可以看到数据真的添加了。

我自己写的Add button事件的代码:

我通过判断返回值是否正常,来手动refetch,并没有更新list数据,为什么?

核心原因是:mutate()不会 立即返回结果,也不会返回一个 Promise 或服务器响应。这里的res实际上是undefined。

怎么做呢?

要么在useMutation里面加onSuccess回调,使用下节课会学习到的query invalidation。

image-20251108204824696

22 - Query Invalidation

在新增成功之后,能不能自动refetch呢?可以,需要使用Invalidations from mutations这个特性。在mutation的onSuccess事件里面添加useQueryClientinvalidateQueries方法。

image-20251109130916454

invalidation:失效。让查询失效。

这个字面意思很难理解。因为这涉及到react-query的执行机制。简单点说,就是:让某个缓存的数据“失效(invalidate)”,从而触发重新获取(refetch)

可以看到新增之后,列表数据自动更新了。

执行过程如下:

  1. 用户添加一个新英雄;
  2. mutation 调用成功;
  3. 执行 invalidateQueries({queryKey: ["super-heroes"]});使这个查询缓存的结果失效。
  4. React Query 把这个 key 的缓存标记为「stale(过期)」;
  5. 如果有组件正在使用这个数据(比如列表页),React Query 会自动 重新发请求
  6. UI 自动更新 ✅。

23 - Handling Mutation Response

因为新增成功之后的返回结果是新增的对象,所以可以拿到这个对象直接添加到列表缓存数据里面去,节省一次网络请求。

可以看到,在新增之后,没有发起列表查询请求,但是列表更新了。

24 - Optimistic Updates

乐观更新。

💬 定义:乐观更新是一种用户体验优化技巧,意思是: 在服务器还没返回成功之前,就先在 UI 上“假设成功”,直接更新界面。如果请求失败,再“回滚”到旧状态。

通常流程是这样的:

普通模式乐观更新模式
1️⃣ 用户点击按钮1️⃣ 用户点击按钮
2️⃣ 显示 loading...2️⃣ 立即把 “Thor” 加到界面上(假装成功)
3️⃣ 请求成功后再更新3️⃣ 请求成功时保持现状(虽然onSettled里面会重新请求数据,但是我们在onMutate里面添加的数据和后端的数据在页面上展示的内容应该是一致的,所以UI不会有变化)
4️⃣ 请求失败再提示错误4️⃣ 请求失败 → 回滚 UI(删除 “Thor”)

22、23两节课学习了在新增成功之后再更新列表数据,这节课来学习新增点击之后就理解更新UI,然后再发post请求,post请求完成后发起列表get请求。

可以看到,界面更新了之后,列表请求才发起,说明速度还是非常快的。

我把post请求的地址修改一下,看一下请求错误的情况。

gif可能看不到,但是我在录制的时候看到了,列表数据显示闪了一下显示新增的数据,但是因为接口报错,所有回滚了数据,之后重新发起了列表数据请求。

乐观更新使用场景:

场景示例
✅ 新增数据添加评论、添加任务、添加列表项
✅ 删除数据删除评论、删除 todo
✅ 点赞 / 收藏点赞按钮立刻变亮
✅ 切换状态开关、切换模式

await queryClient.cancelQueries({ queryKey: ['super-heroes'] });,为什么要加上这一句?

1、告诉 React Query:别再让 ['super-heroes'] 的请求去更新缓存;

2、等当前 mutation 完成后,我们再重新请求(invalidateQueries)。

25 - Axios Interceptor

这节课学习怎么封装axios请求。

在组件里面使用看一下:

使用正常: